﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  WorkflowMessageSender.cs
//    
// Abstract:
//
//  This module is an abstract implementation of IMessageSender for delivering 
//  NotificationItems via a channel managed by Windows Workflow.
//  
//--

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Workflow.Runtime;
using Microsoft.OfficeCommunicationsServer.Applications.Common;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Workflow.Activities;
using System.Workflow.ComponentModel.Compiler;
using System.Text;


namespace Microsoft.EnhancedReminders.Dispatcher.Workflow
{
    internal enum WorkflowCallState
    {
        Uninitialized,
        Dialing,
        Connected,
        Terminating,
        HangingUp
    }

    /// <summary>
    /// Represents an implementation of IMessageSender that uses a Windows
    /// Workflow Activity for managing the delivery of a notification.
    /// </summary>
    public abstract class WorkflowMessageSender : IMessageSender
    {
        internal readonly object _syncObj = new object();
        internal WorkflowCallState _state;

        internal Conversation _conversation;
        internal WorkflowInstance _instance;
        internal WorkflowRuntime _workflowRuntime;
        internal NotificationItem _message;
        internal Call _call;

        /// <summary>
        /// ctor.
        /// </summary>
        protected WorkflowMessageSender(Conversation conversation, WorkflowRuntime runtime, NotificationItem message)
        {
            _conversation = conversation;
            _message = message;
            _workflowRuntime = runtime;
        }

        /// <summary>
        /// Get the Type object for the Workflow class this sender should use.
        /// </summary>
        protected abstract Type WorkflowType
        {
            get;
        }

        /// <summary>
        /// When a workflow completes successfully we need to clean up the workflow
        /// instances from the workflow runtime.
        /// </summary>
        private void WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
        {
            Debug.Assert(_instance != null, "_instance != null");

            if (e.WorkflowInstance.InstanceId == _instance.InstanceId)
            {
                Logger.WriteTraceInformation(_message.Context, "Workflow completed.");

                CleanUpWorkflow();
                OnDeliveryCompleted(Helpers.AssertCast<NotificationResult>(e.OutputParameters["MessageResult"]));
                TerminateCall();
            }
        }

        /// <summary>
        /// When a workflow terminates prematurely we need to clean up the workflow
        /// instances from the workflow runtime.
        /// </summary>
        private void WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            Debug.Assert(_instance != null, "instance != null");

            if (e.WorkflowInstance.InstanceId == _instance.InstanceId)
            {
                if (e.Exception != null)
                {
                    Logger.WriteTraceWarning(_message.Context, e.Exception, "Workflow terminated.");
                }
                else
                {
                    Logger.WriteTraceWarning(_message.Context, "Workflow terminated, but no exception present.");
                }

                CleanUpWorkflow();
                OnDeliveryCompleted(new NotificationResult(
                    _message, NotificationResultStatus.Error, NotificationChannelType.None));
                TerminateCall();
            }
        }

        /// <summary>
        /// Removes event handlers that we added to the workflow runtime.
        /// </summary>
        private void CleanUpWorkflow()
        {
            Logger.WriteTraceVerbose(_message.Context, "Cleaning up workflow event handlers.");

            _instance.WorkflowRuntime.WorkflowCompleted -= WorkflowCompleted;
            _instance.WorkflowRuntime.WorkflowTerminated -= WorkflowTerminated;
        }

        /// <summary>
        /// Handles changes in call state; if the remote party ends the call prematurely,
        /// we need to clean up objects on our end.
        /// </summary>
        private void CallStateChanged(object sender, CallStateChangedEventArgs e)
        {
            Logger.WriteTraceInformation(_message.Context, "Call state changed from {0} to {1}. Reason: {2}", e.PreviousState, e.State, e.TransitionReason);

            lock (_syncObj)
            {
                if (e.State == CallState.Terminated && _state == WorkflowCallState.Dialing)
                {
                    _state = WorkflowCallState.Terminating;
                }
            }

            if (e.State == CallState.Terminated && _state == WorkflowCallState.Connected)
            {
                Logger.WriteTraceWarning(_message.Context, "Call ended prematurely.");
                TerminateCall();
            }
        }

        /// <summary>
        /// Called when a call has been established.
        /// </summary>
        protected virtual void EstablishCompleted(IAsyncResult asyncResult)
        {
            Debug.Assert(asyncResult != null, "asyncResult != null");
            Logger.WriteTraceInformation(_message.Context, "Call established.");

            CallMessageData messageData = null;

            try
            {
                messageData = _call.EndEstablish(asyncResult);
            }
            catch (FailureResponseException ex)
            {
                if (ex.ResponseData != null)
                {
                    Logger.WriteTraceInformation(_message.Context, "Failed to connect call. Reason: {0} {1}", ex.ResponseData.ResponseCode, ex.ResponseData.ResponseText);
                }
                else
                {
                    Logger.WriteTraceInformation(_message.Context, "Failed to connect call. Reason: {0}", ex.DiagnosticInformation);
                }
                TerminateCall();

                OnDeliveryCompleted(new NotificationResult(
                    _message, NotificationResultStatus.Error, NotificationChannelType.None));

                return;
            }
            catch (OperationFailureException ex)
            {
                FailureResponseException innerException = ex.InnerException as FailureResponseException;

                if (innerException != null && innerException.ResponseData != null)
                {
                    Logger.WriteTraceInformation(_message.Context, "Outbound call failed. Reason: {0} {1}", innerException.ResponseData.ResponseCode, innerException.ResponseData.ResponseText);
                }
                else
                {
                    Logger.WriteTraceInformation(_message.Context, "Other party declined the call. Reason {0}", ex.FailureReason);
                }

                TerminateCall();
                OnDeliveryCompleted(new NotificationResult(
                    _message, NotificationResultStatus.Error, NotificationChannelType.None));

                return;
            }

            // If we've contacted someone's voicemail then we should terminate the call and give up on this channel.
            if (_call.RemoteEndpoint.EndpointType == EndpointType.Application && _call.RemoteEndpoint.EndpointSubtype== EndpointSubtype.MessageTaker)
            {
                Logger.WriteTraceInformation(_message.Context, "Call answered by voicemail; hanging up call.");

                TerminateCall();
                OnDeliveryCompleted(new NotificationResult(
                    _message.Context, _message.Id, NotificationResultStatus.Error, NotificationChannelType.None));
                return;
            }

            lock (_syncObj)
            {
                if (_state == WorkflowCallState.Dialing)
                {
                    _state = WorkflowCallState.Connected;
                }
            }

            // it's possible that the other party may have hung up the call between the call to EndEstablish and
            // now. If so, do not proceed.
            if (_state == WorkflowCallState.Terminating || _state == WorkflowCallState.HangingUp)
            {
                TerminateCall();
            }
            else
            {
                Debug.Assert(messageData != null, "messageData != null");
                Logger.WriteTraceInformation(_message.Context, "Beginning call flow.");

                
                CommunicationsWorkflowRuntimeService objectService = _instance.WorkflowRuntime.GetService<CommunicationsWorkflowRuntimeService>();
                objectService.SetWorkflowCulture(_instance.InstanceId, CultureInfo.CurrentUICulture);
                objectService.EnqueueCall(_instance.InstanceId, _call);
                _instance.Start();

            }
        }

       

        /// <summary>
        /// Begins terminating the call. Concreate subclasses are expected to clean up
        /// any specific resources.
        /// </summary>
        protected virtual void TerminateCall()
        {
            Debug.Assert(_state != WorkflowCallState.Uninitialized, "_state != WorkflowCallState.Uninitialized");
            Logger.WriteTraceInformation(_message.Context, "Terminating call.");

            lock (_syncObj)
            {
                if (_state == WorkflowCallState.HangingUp)
                {
                    return;
                }

                _state = WorkflowCallState.HangingUp;
            }

            if (_call != null && _call.State == CallState.Established)
            {
                Logger.WriteTraceVerbose(_message.Context, "Calling BeginTerminate.");
                _call.StateChanged -= CallStateChanged;
                _call.BeginTerminate(TerminateCompleted, _call);
            }
        }

        private void TerminateCompleted(IAsyncResult asyncResult)
        {
            Logger.WriteTraceInformation(_message.Context, "Hang up complete.");
            _call.EndTerminate(asyncResult);
            asyncResult.AsyncWaitHandle.Close();
        }

        /// <summary>
        /// Fires the DeliveryCompleted event to any listeners.
        /// </summary>
        protected virtual void OnDeliveryCompleted(NotificationResult result)
        {
            EventHandler<DeliveryCompletedEventArgs> callback = DeliverMessageCompleted;

            CleanupCall();

            if (callback != null)
            {
                Logger.WriteTraceInformation(this._message.Context, "Delivery completed.");
                callback(this, new DeliveryCompletedEventArgs(result));
            }
        }

        #region IMessageSender Members

        /// <summary>
        /// Event to be fired when an attempt to deliver a notification is complete
        /// </summary>
        public event EventHandler<DeliveryCompletedEventArgs> DeliverMessageCompleted;

        /// <summary>
        /// Asynchronously attempts to deliver a notification
        /// </summary>
        public virtual void DeliverMessageAsync()
        {
            Debug.Assert(_state == WorkflowCallState.Uninitialized, "_state == WorkflowCallState.Uninitialized");

            CreateWorkflowInstance();

            _state = WorkflowCallState.Dialing;

            try
            {
                if (InitializeCall())
                {
                    // This outputs the object type of the call to make which type of call is being made clear.
                    _call.StateChanged += CallStateChanged;
                    Logger.WriteTraceInformation(_message.Context, "Establishing Call of type {0}", _call.GetType());
		            CallEstablishOptions options = new CallEstablishOptions();
                    string targetAddress = _conversation.ApplicationContext as string;
                    _call.BeginEstablish(targetAddress, options, EstablishCompleted, _call);
                }
            }
            catch (RealTimeException e)
            {
                OperationalLogs.LogVoiceNotificationError(_message.EmailAddress, e);
                OnDeliveryCompleted(new NotificationResult(_message, NotificationResultStatus.Error, NotificationChannelType.Communicator));
            }
        }

        #endregion

        /// <summary>
        /// Create and initialize an instance of the workflow to execute this call.
        /// </summary>
        private void CreateWorkflowInstance()
        {
            _instance = _workflowRuntime.CreateWorkflow(WorkflowType, GenerateWorkflowParameters());
            _instance.WorkflowRuntime.WorkflowCompleted += WorkflowCompleted;
        }
        

        /// <summary>
        /// Generates the parameters to pass to the workflow instance.
        /// </summary>
        /// <returns></returns>
        protected virtual Dictionary<string, object> GenerateWorkflowParameters()
        {
            Dictionary<string, object> workflowParameters = new Dictionary<string, object>();
            workflowParameters.Add("Message", _message);

            return workflowParameters;
        }

        /// <summary>
        /// Performs any setup required by concrete subclasses.
        /// </summary>
        /// <returns>True if initialization completed successfully and the call should continue, false otherwise.</returns>
        protected abstract bool InitializeCall();

        /// <summary>
        /// Performs and cleanup steps required by concrete subclasses.
        /// </summary>
        protected abstract void CleanupCall();
    }
}
